Совместное использование Unity и ASP.NET MVC

В этой статье я опишу, как использовать библиотеку Microsoft Unity в приложениях ASP.NET MVC. А также, какие преимущества от этого можно получить, в частности для написания unit-тестов.

Как известно, Unity представляет собой реализацию паттерна Dependency Injection (DI). Кратко опишу, что собой представляет DI(“ внедрение зависимостей”). Представим, что некий объект использует в своей работе какой-то сервис. Ссылка на сервис хранится в приватном поле объекта. Для того, что бы объект не был жестко связан с сервисом (coupling), мы можем применить «внедрение зависимостей». DI представляет механизм, позволяющий в момент создания объекта извне инициализировать ссылку на сервис. Как правило, это делается через публичное свойство или параметризованный конструктор.

И так, для начала рассмотрим, как выглядит простое MVC приложение, без использования Unity. Предположим, что у нас есть контроллер, передающий во view список пользователей. Контроллер обращается к статическому методу класса DataHelper, для получения списка пользователей. DataHelper, в свою очередь, обращается к базе данных.

<code>public class DataHelper
{
  public static List<User> GetAllUsers()
  {
    //connect to DB and fetch list of users
  }      
}

public class UserController
{
  public ActionResult ShowAll()
  {
    return View(DataHelper.GetAllUsers().ToList());
  }
}

* This source code was highlighted with Source Code Highlighter.


В данной реализации, мы видим сильную “связанность” классов UserController и DataHelper.
И как следствие этого, есть проблема с тестированием метода ShowAll. Мы не можем протестировать этот метод, без вызова метода GetAllUsers и соответственно обращения к базе данных.

Теперь, рассмотрим вариант с использованием Unity. Первое, что мы сделаем – избавимся от статического метода в DataHelper и выделим интерфейс. А заодно переименуем класс DataHelper в DataService.
public interface IDataService
{
  List<User> GetAllUsers();
}

public class DataService : IDataService
{
  public List<User> GetAllUsers()
  {
    //connect to DB and fetch list of users
  }      
}

* This source code was highlighted with Source Code Highlighter.


Внесем изменения в реализацию контроллера: добавим публичное свойство DataService, в котором будет храниться ссылка на экземпляр IDataService, и пометим это свойство атрибутом Dependency.
[Dependency]
public IDataService DataService { get; set; }
public class UserController
{
  public ActionResult ShowAll()
  {
    return View(DataService.GetAllUsers().ToList());
  }
}

* This source code was highlighted with Source Code Highlighter.


Теперь осталось настроить собственно сам Unity. Мы сделаем это в модуле Global.ascx.
protected void Application_Start()
{
  …
  InitContainer();
}    

private static UnityContainer _container;
private static void InitContainer()
{
  if (_container == null)
    _container = new UnityContainer();

  IControllerFactory controllerFactory = new UnityControllerFactory(_container);
  ControllerBuilder.Current.SetControllerFactory(controllerFactory);

  _container.RegisterType<IDataService, DataService>(new ContainerControlledLifetimeManager());
}

* This source code was highlighted with Source Code Highlighter.


Обратите внимание, что в методе InitContainer мы регистрируем тип и его реализацию, который Unity будет внедрять, основываясь на атрибуте Dependency. При этом в качестве параметра метода RegisterType передается экземпляр ContainerControlledLifetimeManager. Это значит, что каждый раз, при вызове метода контейнера Resolve, будет возвращаться один и тот же экземпляр DataService.

И последнее, что нам осталось сделать, это реализовать свою собственную фабрику контроллеров UnityControllerFactory. Эта фабрика будет возвращать экземпляр котроллера UserController уже проинициализированный экземпляром DataService.
public class UnityControllerFactory : DefaultControllerFactory
{
  IUnityContainer _container;

  public UnityControllerFactory(IUnityContainer container)
  {
    _container = container;
  }

  protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
  {
    if (controllerType == null)
      throw new ArgumentNullException("controllerType");

    if (!typeof(IController).IsAssignableFrom(controllerType))
      throw new ArgumentException(string.Format(
        "Type requested is not a controller: {0}", controllerType.Name),
        "controllerType");

    return _container.Resolve(controllerType) as IController;
  }
}

* This source code was highlighted with Source Code Highlighter.


На этом этапе мы реализовали использование Unity в приложении MVC. Теперь можем написать unit test метода ShowAll класса UserController. Сначала создаем mock класс:
public class TestDataService : IDataService
{
  public List<User> GetAllUsers()
  {
    return new List<User>
      {
        new User
           {
              FirstName = "firstname",
              LastName = "lastname",
              Age = "34"
            }, 
        new User
           {
               FirstName = "firstname1",
               LastName = "lastname1",
               Age = "31"
            }
      }
  }
}

* This source code was highlighted with Source Code Highlighter.


И собственно сам тест:
[TestMethod]
public void ShowAllTest
{
  var container = new UnityContainer();
  container.RegistryType<IDataService, TestDataService>();

  var userController = container.Resolve<UserController>();
  ViewResult result = controller.ShowAll() as ViewResult;
  
  var list = result.ViewData.Model as IEnumerable<User>
  Assert.AreEqual(2, list.Count());
}

* This source code was highlighted with Source Code Highlighter.


Теперь проверяется код только метода ShowAll, и он не зависит от класса DataService и тем более от базы данных.

В заключении хочу отметить два момента. В production мы можем настраивать контейнер Unity декларативно, с использованием конфигурационного файла. Т.о., для перехода на тестовый environment, нам не надо изменять код, достаточно поменять конфигурационный файл. И второе, «внедрение зависимостей» мы можем производить не только через публичное свойство, но и через параметризованный конструктор. И в том, и в другом случае такие члены должны помечаться атрибутом Dependency.